# 04장 설계 품질과 트레이드오프

Assembled by GimunLee

# Introduction

설계는 변경을 위해 존재하고 변경에는 어떤 식으로든 비용이 발생한다. 훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다. 적절한 비용 안에서 쉽게 변경할 수 있는 설계는 응집도가 높고 서로 느슨하게 결합돼 있는 요소로 구성된다.

결합도와 응집도를 합리적인 수준으로 유지할 수 있는 중요한 원칙이 있다. 객체의 상태가 아니라 객체의 행동에 초점을 맞추는 것이다. 객체를 단순한 데이터의 집합으로 바라보는 시작은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시키는 결과를 낳기 때문에 결과적으로 설계가 변경에 취약해진다. 이런 문제를 피할 수 있는 가장 좋은 방법은 객체의 책임에 초점을 맞추는 것이다. 책임은 객체의 상태에서 행동으로, 나아가 객체와 객체 사이의 상호작용으로 설계 중심을 이동시키고, 결합도가 낮고 응집도가 높으며 구현을 효과적으로 캡슐화하는 객체들을 창조할 수 있는 기반을 제공한다.


# 01. 데이터 중심의 영화 예매 시스템

데이터 중심의 관점에서 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의한다.

책임 중심의 관점에서 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다.

데이터 중심의 관점은 객체의 상태에 초점을 맞추고 책임 중심의 관점은 객체의 행동에 초점을 맞춘다. 전자는 객체를 독립된 데이터 덩어리로 바라보고 후자는 객체를 협력하는 공동체의 일원으로 바라본다.

# 데이터를 준비하자

책임 중심 설계가 '책임이 무엇인가'를 묻는 것으로 시작한다면 데이터 중심의 설계는 객체가 내부에 저장해야 하는 '데이터가 무엇인가'를 묻는 것으로 시작한다. 먼저 Movie에 저장될 데이터를 결정하는 것으로 설계를 시작하자.

구현 내용 (책 99p ~)


# 02. 설계 트레이드오프

객체지향 커뮤니티에서는 오랜 기간 동안 좋은 설계의 특징을 판달할 수 있는 기준에 관한 다양한 논의가 있어 왔다. 여기서는 데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도를 사용하겠다.

# 캡슐화

상태와 행동을 하나의 객체 안에 모이는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다. 여기서 구현이란 나중에 변경될 가능성이 높은 어떤 것을 가리킨다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다. 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적은 부분을 인터페이스라고 부른다는 사실을 기억하라.

# 응집도와 결합도

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다. 모듈 내의 요소들이 서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가진다. 객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

결합도는 의존성의 정도를 나타내며 다른 모듈에 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.


# 03. 데이터 중심의 영화 예매 시스템의 문제점

데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다. 반면 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다.

캡슐화의 정도가 객체의 응집도와 결합도를 결정한다는 사실을 기억하라. 데이터 중심의 설계는 캡슐화를 위반하기 쉽기 때문에 책임 중심의 설계에 비해 응집도가 낮고 결합도가 높은 객체들을 양산하게 될 가능성이 높다.

요약하자면 데이터 중심의 설계가 가진 대표적인 문제점을 다음과 같이 요약할 수 있다.

  • 캡슐화 위반
  • 높은 결합도
  • 낮은 응집도

각 문제점을 좀 더 자세히 살펴보자

# 캡슐화 위반

public class Movie {
	private Money fee;
	
	public Money getFee() {
		return fee;
	}
	
	public void setFee(Money fee) {
		this.fee = fee;
	}
}

위 코드는 직접 객체의 내부에 접근할 수 없기 때문에 캡슐화의 원칙을 지키고 있는 것처럼 보인다. 하지만 안타깝게도 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화 하지 못한다. getFee 메서드와 setFee 메서드는 Movie 내부에 Money 타입의 fee라는 이름의 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러낸다.

Movie가 캡슐화의 원칙을 어기게 된 근본적인 원인은 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다. 객체에게 중요한 것은 책임이다. 그리고 구현을 캡술화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다.

오픈한 게 많을수록 이 데이터를 쓰는 연관된 곳이 많다는 이야기이다. 그래서 변경에 취약하다.

추측에 의한 설계 전략(design-by-guessing strategy)

# 높은 결합도

지금까지 살펴본 것처럼 데이터 중심의 설계는 접근자와 수정자를 통해 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다. 객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다. 그리고 더 나쁜 소식을 단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다는 것이다.

public class ReservationAgency {
	public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
		...
		Money fee;
		if(discountable) {
			...
			fee = movie.getFee().minus(discountedAmount).times(audienceCount);
		} else {
			fee = movie.getFee();
		}
	}
}

여기서 movie.getFee()의 반환 타입이 Money가 아닌 다른 것으로 바뀐다고 가정하면 위의 코드도 수정해야 한다. fee의 타입 변경으로 인해 협력하는 클래스가 변경되기 때문에 getFee 메서드는 fee를 정상적으로 캡슐화하지 못한다. 사실 getFee 메서드를 사용하는 것은 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다. 이처럼 데이터 중심 설계는 객체의 캡슐화를 약화시키기 때문에 클라이언트가 객체의 구현에 강하게 결합된다.

# 낮은 응집도

ReservationAgency를 예로 들어 변경과 응집도 사이의 관계를 살펴보자. 아마 다음과 같은 수정사항이 발생하는 경우에 ReservationAgency의 코드를 수정해야 할 것이다.

  • 할인 정책이 추가될 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 예매 요금을 계산하는 방법이 변경될 경우

낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다.

  • ReservationAgency 안에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 존재하기 때문에 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다. 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.

  • 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 새로운 할인 정책을 추가해야 한다고 가정해 보자. 이를 위해서는 MovieType에 새로운 할인 정책을 표현하는 열거형 값을 추가하고 ReservationAgency의 reserve 메서드의 switch 구문에 새로운 case 절을 추가해야 한다. 또한 새로운 할인 정책에 따라 할인 요금을 계산하기 위해 필요한 데이터도 Movie에 추가해야 한다. 하나의 요구사항 변화를 수용하기 위해 MovieType, ReservationAgency, Movie라는 세 개의 클래스를 함께 수정해야 하는 것이다.

# 단일 책임 원칙(Single Responsibility Principle, SRP)

로버트 마틴은 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제시했다. 단일 책임 원칙을 한마디로 요약하면 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다. 아마 방금 전에 설명한 내용을 이해했다면 단일 책임 원칙이 클래스의 응집도를 높일 수 있는 설계 원칙이라는 사실을 이해했을 것이다.

한 가지 주의할 점은 단일 책임 원칙이라는 맥락에서 '책임'이라는 말이 '변경의 이유'라는 의미로 사용된다는 점이다. 단일 책임 원칙에서의 책임은 지금까지 살펴본 역할, 책임, 협력에서 이야기하는 책임과는 다르며 변경과 관련된 더 큰 개념을 가리킨다.


# 04. 자율적인 객체를 향해

# 캡슐화를 지켜라

객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다. 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.

여기서 말하는 메서드는 단순히 속성 하나의 값을 반환하거나 변경하는 접근자나 수정자를 의미하는 것은 아니다. 객체에게 의미 있는 메서디와 객체가 책임져야 하는 무언가를 수행하는 메서드다. 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.

getter, setter는 데이터 무결성을 위해 사용된다.

# 스스로 자신의 데이터를 책임지는 객체

우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

따라서 객체를 설계할 때 "이 객체가 어떤 데이터를 포함해야 하는가?"라는 질문은 다음과 같은 두 개의 개별적인 질문으로 분리해야 한다.

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

두 질문을 조합하면 객체의 내부 상태를 저장하는 방식과 저장된 상태에 대해 호출할 수 있는 오퍼레이션의 집합을 얻을 수 있다. 다시 말해 새로운 데이터 타입을 만들 수 있다는 것이다.

구현 내용 (책 120p ~)


# 05. 하지만 여전히 부족하다

# 캡슐화 위반

분명히 수정된 객체들은 자기 자신의 데이터를 스스로 처리한다. 하지만 DiscountCondition과 Movie를 살펴보면 내부 구현을 인터페이스에 노출 시키고 있다.

이 예제는 캡슐화가 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다는 것을 잘 보여준다. 사실 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 내부 속성을 외부로부터 감추는 것은 '데이터 캡슐화'라고 불리는 캡슐화의 한 종류일 뿐이다.

다시 한번 강조하지만 캡슐화란 변할 수 있는 어떤 것이라도 감추는 것이다. 그것이 속성의 타입이건, 할인 정책의 종류건 상관 없이 내부 구현의 변경으로 인해 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다. 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화해야 한다.

정리하면 캡슐화란 변하는 어떤 것이든 감추는 것이다. 그것이 무엇이든 구현과 관련된 것이라면 말이다.

# 높은 결합도

인터페이스가 아니라 구현을 변경하는 경우에도 DiscountCondition에 의존하는 Movie를 변경해야 한다는 것은 두 객체 사이의 결합도가 높다는 것을 의미한다.

모든 문제의 원인은 캡슐화 원칙을 지키지 않았기 때문이다. 다시 한번 강조하지만 유연한 설계를 창조하기 위해서는 캡슐화를 설계의 첫 번째 목표로 삼아야 한다.

# 낮은 응집도

위와 같은 경우에 DiscountCondition을 수정하면 Movie도 수정해야하고, Movie를 사용하는 Screening을 함께 수정해야 한다. 하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다는 것은 설계의 응집도가 낮다는 증거이다.


# 데이터 중심 설계의 문제점

데이터 중심의 설계가 변경에 취약한 이유는 두 가지다.

  • 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
  • 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

# 데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다.

데이터 중심 설계 방식에 익숙한 개발자들은 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 따른다. 이것은 상태와 행동을 하나의 단위로 캡슐화하는 객체지향 패러다임에 반하는 것이다. 데이터 중심의 관점에서 객체는 그저 단순한 데이터의 집합체일 뿐이다. 이로 인해 접근자와 수정자를 과도하게 추가하게 되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다.

# 데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.

객체지향 애플리케이션을 구현한다는 것은 협력하는 객체들의 공동체를 구축한다는 것을 의미한다. 따라서 협력이라는 문맥 안에서 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다. 올바른 객체지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다. 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방법이다.

Last Updated: 8/12/2020, 1:33:42 PM